package no.nordicsemi.support.v18.scanner;

import ohos.agp.components.Clock;
import ohos.app.Context;
import ohos.bluetooth.ble.BleScanFilter;
import ohos.event.intentagent.IntentAgent;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * This class provides methods to perform scan related operations for Bluetooth LE devices. An
 * can also request different types of callbacks for delivering the result.
 * <p>
 * Use {@link BluetoothLeScannerCompat#getScanner()} to get an instance of the scanner.
 * <p>
 * Since version 1.3.0 of the Scanner Compar Library library,
 * whether the scanning with {@link IntentAgent} feature is used or not.
 * filtering.
 * <p>
 * <b>Note:</b> Most of the scan methods here require
 */
@SuppressWarnings("WeakerAccess")
public abstract class BluetoothLeScannerCompat {

    /**
     * Extra containing a list of ScanResults. It can have one or more results if there was no
     * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this
     * extra will not be available.
     */
    public static final String EXTRA_LIST_SCAN_RESULT =
            "bluetooth.le.extra.LIST_SCAN_RESULT";
    /**
     * Optional extra indicating the error code, if any. The error code will be one of the
     * SCAN_FAILED_* codes in {@link ScanCallback}.
     */
    public static final String EXTRA_ERROR_CODE = "bluetooth.le.extra.ERROR_CODE";
    /**
     * Optional extra indicating the callback type, which will be one of
     */
    public static final String EXTRA_CALLBACK_TYPE = "bluetooth.le.extra.CALLBACK_TYPE";
    private static BluetoothLeScannerCompat instance;

    /**
     * Returns the scanner compat object
     */
    public BluetoothLeScannerCompat() {
    }

    public synchronized static BluetoothLeScannerCompat getScanner() {
        if (instance != null)
            return instance;
        instance = new BluetoothScannerImplV5();
        return instance;
    }

    /**
     * Start Bluetooth LE scan with default parameters and no filters. The scan results will be
     * delivered through {@code callback}.
     * <p>
     * An app must hold
     * in order to get results.
     *
     * @param context context
     * @param callback Callback used to deliver scan results.
     * @throws IllegalArgumentException If {@code callback} is null.
     */
    @SuppressWarnings("WeakerAccess")
    public final void startScan(Context context, final ScanCallback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback is null");
        }
        final EventHandler handler = new EventHandler(EventRunner.getMainEventRunner());
        startScanInternal(context, Collections.<BleScanFilter>emptyList(), new ScanSettings.Builder().build(),
                callback, handler);
    }

    /**
     * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
     * <p>
     * An app must hold
     * in order to get results.
     *
     * @param filters filters
     * @param context context
     * @param settings Optional settings for the scan.
     * @param callback Callback used to deliver scan results.
     * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
     */
    public final void startScan(Context context, @Nullable final List<BleScanFilter> filters,
                                @Nullable final ScanSettings settings,
                                final ScanCallback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback is null");
        }
        final EventHandler handler = new EventHandler(EventRunner.getMainEventRunner());
        startScanInternal(context, filters != null ? filters : Collections.<BleScanFilter>emptyList(),
                settings != null ? settings : new ScanSettings.Builder().build(),
                callback, handler);
    }

    /**
     * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
     * <p>
     * An app must hold
     * in order to get results.
     *
     * @param context context
     * @param scanFilters scanFilters
     * @param scanSettings scanSettings
     * @param callback Callback used to deliver scan results.
     * @param handler  Optional handler used to deliver results.
     * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
     */
    protected abstract void startScanInternal(Context context, List<BleScanFilter> scanFilters, ScanSettings scanSettings, ScanCallback callback, EventHandler handler);

    /**
     * Stops an ongoing Bluetooth LE scan.
     * <p>
     *
     * @param callback The callback used to start scanning.
     */
    public final void stopScan(final ScanCallback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback is null");
        }
        stopScanInternal(callback);
    }

    abstract void stopScanInternal(ScanCallback callback);

    /**
     * which will scan in background using given settings.
     * <p>
     * This method of scanning is intended to work in background. Long running scanning may
     * consume a lot of battery, so it is recommended to use low power mode in settings,
     * offloaded filtering and batching. However, the library may emulate batching, filtering or
     * <p>
     * whether this feature is used or not.
     * <p>
     * An app must hold
     * in order to get results.
     * <p>
     * When the PendingIntent is delivered, the Intent passed to the receiver or activity will
     * contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE}, {@link #EXTRA_ERROR_CODE} and
     * {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of the scan.
     *
     * @param filters filters
     * @param settings settings
     * @param context context
     * @param callbackIntent The PendingIntent to deliver the result to.
     * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
     */
    public final void startScan(@Nullable final List<BleScanFilter> filters,
                                @Nullable final ScanSettings settings,
                                final Context context,
                                final IntentAgent callbackIntent) {
        if (context == null) {
            throw new IllegalArgumentException("context is null");
        }
        startScanInternal(filters != null ? filters : Collections.<BleScanFilter>emptyList(),
                settings != null ? settings : new ScanSettings.Builder().build(),
                context, callbackIntent);
    }

    /**
     * Stops an ongoing Bluetooth LE scan.
     * <p>
     *
     * @param context context
     * @param callbackIntent The PendingIntent that was used to start the scan.
     */
    public final void stopScan(final Context context,
                               final IntentAgent callbackIntent) {
        if (callbackIntent == null) {
            throw new IllegalArgumentException("callbackIntent is null");
        }
        if (context == null) {
            throw new IllegalArgumentException("context is null");
        }
        stopScanInternal(context, callbackIntent);
    }

    /**
     * Starts Bluetooth LE scan using PendingIntent.
     * @param callbackIntent The PendingIntent to deliver the result to.
     */
    /* package */
    abstract void startScanInternal(List<BleScanFilter> filters,
                                    ScanSettings settings,
                                    Context context,
                                    IntentAgent callbackIntent);

    /**
     * Stops an ongoing Bluetooth LE scan.
     *
     * @param callbackIntent The PendingIntent that was used to start the scan.
     */
    /* package */
    abstract void stopScanInternal(Context context,
                                   IntentAgent callbackIntent);

    /**
     * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth
     * LE scan results batched on Bluetooth controller. Returns immediately, batch scan results data
     * will be delivered through the {@code callback}.
     *
     * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one
     *                 used to start scan.
     */
    @SuppressWarnings("unused")
    public abstract void flushPendingScanResults(ScanCallback callback);

    public abstract ArrayList<ScanResult> flushPendingScanResults(List<ScanResult> nativeScanResults);

    /* package */ static class ScanCallbackWrapper {
        private final Object LOCK = new Object();
        private final boolean emulateFiltering;
        private final boolean emulateBatching;
        private boolean scanningStopped;

        final List<BleScanFilter> filters;
        final ScanSettings scanSettings;
        final ScanCallback scanCallback;
        final EventHandler handler;
        Context context;

        private final List<ScanResult> scanResults = new ArrayList<>();

        private final Set<String> devicesInBatch = new HashSet<>();

        /**
         * A collection of scan result of devices in range.
         */
        private final Map<String, ScanResult> devicesInRange = new HashMap<>();

        private final Runnable flushPendingScanResultsTask = new Runnable() {
            @Override
            public void run() {
                if (!scanningStopped) {
                    flushPendingScanResults();
                    handler.postTask(this, scanSettings.getReportDelayMillis());
                }
            }
        };

        /**
         * A task, called periodically, that notifies about match lost.
         */
        private final Runnable matchLostNotifierTask = new Runnable() {
            @Override
            public void run() {
                Clock clock = new Clock(context);
                long now = clock.getTime();
                synchronized (LOCK) {
                    final Iterator<ScanResult> iterator = devicesInRange.values().iterator();
                    while (iterator.hasNext()) {
                        final ScanResult result = iterator.next();
                        if (result.getTimestampNanos() < now - scanSettings.getMatchLostDeviceTimeout()) {
                            iterator.remove();
                            handler.postTask(new Runnable() {
                                @Override
                                public void run() {
                                    scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST, result);
                                }
                            });
                        }
                    }
                    if (!devicesInRange.isEmpty()) {
                        handler.postTask(this, scanSettings.getMatchLostTaskInterval());
                    }
                }
            }
        };

        /* package */ ScanCallbackWrapper(final boolean offloadedBatchingSupported,
                                          final boolean offloadedFilteringSupported,
                                          final List<BleScanFilter> filters,
                                          final ScanSettings settings,
                                          final ScanCallback callback,
                                          final EventHandler handler) {
            this.filters = Collections.unmodifiableList(filters);
            this.scanSettings = settings;
            this.scanCallback = callback;
            this.handler = handler;
            this.scanningStopped = false;
            emulateFiltering = !filters.isEmpty() && (!offloadedFilteringSupported || !settings.getUseHardwareFilteringIfSupported());
            final long delay = settings.getReportDelayMillis();
            emulateBatching = delay > 0 && (!offloadedBatchingSupported || !settings.getUseHardwareBatchingIfSupported());
            if (emulateBatching) {
                handler.postTask(flushPendingScanResultsTask, delay);
            }
        }

        /* package */ void close() {
            scanningStopped = true;

            synchronized (LOCK) {
                devicesInRange.clear();
                devicesInBatch.clear();
                scanResults.clear();
            }
        }

        /* package */ void flushPendingScanResults() {
            if (emulateBatching && !scanningStopped) {
                synchronized (LOCK) {
                    scanCallback.onBatchScanResults(new ArrayList<>(scanResults));
                    scanResults.clear();
                    devicesInBatch.clear();
                }
            }
        }

        /* package */ void handleScanResult(final int callbackType,
                                            final ScanResult scanResult) {
            if (scanningStopped || !filters.isEmpty() && !matches(scanResult))
                return;
            final String deviceAddress = (String) scanResult.getDevice();
            ScanResult previousResult;
            boolean firstResult;
            synchronized (devicesInRange) {
                firstResult = devicesInRange.isEmpty();
                previousResult = devicesInRange.put(deviceAddress, scanResult);
                if (previousResult == null) {
                    if ((scanSettings.getCallbackType() & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) > 0) {
                        scanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, scanResult);
                    }
                }
                if (firstResult) {
                    if ((scanSettings.getCallbackType() & ScanSettings.CALLBACK_TYPE_MATCH_LOST) > 0) {
                        handler.removeTask(matchLostNotifierTask);
                        handler.postTask(matchLostNotifierTask, scanSettings.getMatchLostTaskInterval());
                    }
                }
                if (emulateBatching) {
                    synchronized (LOCK) {
                        if (!devicesInBatch.contains(deviceAddress)) {
                            scanResults.add(scanResult);
                            devicesInBatch.add(deviceAddress);
                        }
                    }
                    return;
                }
                scanCallback.onScanResult(callbackType, scanResult);
            }
        }

        /* package */ void handleScanResults(final List<ScanResult> results) {
            if (scanningStopped)
                return;
            List<ScanResult> filteredResults = results;

            if (emulateFiltering) {
                filteredResults = new ArrayList<>();
                for (final ScanResult result : results)
                    if (matches(result))
                        filteredResults.add(result);
            }
            scanCallback.onBatchScanResults(filteredResults);
        }
        /* package */ void handleScanError(final int errorCode) {
            scanCallback.onScanFailed(errorCode);
        }

        private boolean matches(final ScanResult result) {
            for (final BleScanFilter filter : filters) {

            }
            return false;
        }
    }
}
